<?php

namespace wchat\wx\V3;

use Exception;
use Kiri\Client;
use wchat\common\Help;


/**
 * Bytes Length of the AES block
 */
const BLOCK_SIZE = 16;

/**
 * The `aes-256-gcm` algorithm string
 */
const ALGO_AES_256_GCM = 'aes-256-gcm';

trait WxV3PaymentTait
{

    /**
     * @param string $orderNo
     * @param int $total
     * @return array
     */
    public function getInitCore(string $orderNo, int $total): array
    {
        $payConfig = $this->getPayConfig();
        if ($payConfig->typeIsApp()) {
            $body['appid'] = $payConfig->pay->wx->appId;
        } else {
            $body['appid'] = $payConfig->appId;
        }
        $body['mchid']        = $payConfig->pay->wx->mchId;
        $body['description']  = $payConfig->getBody();
        $body['out_trade_no'] = $orderNo;
        $body['notify_url']   = $payConfig->getNotifyUrl();
        $body['amount']       = ['total' => $total, 'currency' => $payConfig->getCurrency()];
        return $body;
    }


    /**
     * @param string $sign
     * @param string $json
     * @return Client
     */
    public function createClient(string $sign, string $json): Client
    {
        $client = new Client('api.mch.weixin.qq.com', 80, TRUE);
        $client->withAddedHeader('Authorization', $sign)
               ->withContentType('application/json')->withAddedHeader('User-Agent', 'application/json')
               ->withAddedHeader("Accept", "*/*");
        if (!empty($json)) {
            $client->withBody($json);
        }
        $proxyHost = $this->getPayConfig()->getProxyHost();
        $proxyPort = $this->getPayConfig()->getProxyPort();
        if (!empty($proxyHost) && $proxyPort > 0) {
            $client->withProxyHost($proxyHost)->withProxyPort($proxyPort);
        }
        return $client;
    }


    /**
     * @param string $orderNo
     * @return array
     * @throws
     */
    public function searchByOutTradeNo(string $orderNo): array
    {
        return $this->search('/v3/pay/transactions/out-trade-no/' . $orderNo);
    }


    /**
     * @param string $orderNo
     * @return array
     * @throws
     */
    public function searchByTransactionId(string $orderNo): array
    {
        return $this->search('/v3/pay/transactions/id/' . $orderNo);
    }


    /**
     * @param string $parseUrl
     * @return array
     * @throws
     */
    private function search(string $parseUrl): array
    {
        $config = $this->getPayConfig();

        $sign   = $this->signature('GET', $parseUrl . '?mchid=' . $config->pay->wx->mchId);
        $client = $this->createClient($sign, '');
        $client->withAddedHeader('Accept', 'application/json');
        $client->get($parseUrl, ['mchid' => $config->pay->wx->mchId]);
        $client->close();

        return json_decode($client->getBody(), TRUE);
    }


    /**
     * @param string $http_method
     * @param string $canonical_url
     * @param string $body
     * @return string
     * @throws
     */
    public function signature(string $http_method, string $canonical_url, string $body = ''): string
    {
        $payConfig = $this->getPayConfig();

        $rand = md5(random_bytes(32));
        $time = time();

        $message = sprintf("%s\n%s\n%d\n%s\n%s\n", $http_method, $canonical_url, $time, $rand, $body);

        $sign = $this->openssl_signature($message);

        return sprintf('%s mchid="%s",nonce_str="%s",timestamp="%d",serial_no="%s",signature="%s"', $payConfig->pay->wx->schema,
            $payConfig->pay->wx->mchId, $rand, $time, $payConfig->pay->wx->SerialNumber, $sign);
    }


    /**
     * @param string $body
     * @return string
     */
    public function openssl_signature(string $body): string
    {
        $payConfig = $this->getPayConfig();

        $mch_private_key = openssl_get_privatekey(file_get_contents($payConfig->pay->wx->mchCert));

        openssl_sign($body, $raw_sign, $mch_private_key, 'sha256WithRSAEncryption');
        return base64_encode($raw_sign);
    }


    /**
     * @param array $json
     * @param array $body
     * @return array
     */
    private function createResponse(array $json, array $body): array
    {
        $responseArray['appId']     = $body['appid'];
        $responseArray['timeStamp'] = (string)time();
        $responseArray['nonceStr']  = Help::random(32);
        $responseArray['package']   = "prepay_id=" . $json['prepay_id'];

        $responseBody = $responseArray['appId'] . PHP_EOL . $responseArray['timeStamp'] . PHP_EOL . $responseArray['nonceStr'] . PHP_EOL . $responseArray['package'] . PHP_EOL;

        $responseArray['signType']  = 'RSA';
        $responseArray['signBody']  = $responseBody;
        $responseArray['paySign']   = $this->openssl_signature($responseBody);
        $responseArray['prepay_id'] = $json['prepay_id'];

        return $responseArray;
    }

    /**
     * @param string $ciphertext
     * @param string $v3Key
     * @param string $iv
     * @param string $aad
     * @return array
     */
    public function decrypt(string $ciphertext, string $v3Key, string $iv = '', string $aad = ''): array
    {
        $ciphertext = base64_decode($ciphertext);
        $authTag    = substr($ciphertext, $tailLength = 0 - BLOCK_SIZE);
        $tagLength  = strlen($authTag);

        /* Manually checking the length of the tag, because the `openssl_decrypt` was mentioned there, it's the caller's responsibility. */
        if ($tagLength > BLOCK_SIZE || ($tagLength < 12 && $tagLength !== 8 && $tagLength !== 4)) {
            throw new \RuntimeException('The inputs `$ciphertext` incomplete, the bytes length must be one of 16, 15, 14, 13, 12, 8 or 4.');
        }
        $plaintext = openssl_decrypt(substr($ciphertext, 0, $tailLength), ALGO_AES_256_GCM, $v3Key, OPENSSL_RAW_DATA, $iv, $authTag, $aad);
        if (false === $plaintext) {
            throw new \UnexpectedValueException('Decrypting the input $ciphertext failed, please checking your $key and $iv whether or nor correct.');
        }
        return json_decode($plaintext, true);
    }
}
